/**
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements. See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership. The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License. You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied. See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package org.apache.fineract.infrastructure.openjpa;

import java.sql.Connection;
import java.sql.SQLException;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceException;
import org.apache.commons.logging.LogFactory;
import org.apache.openjpa.persistence.FetchPlan;
import org.apache.openjpa.persistence.OpenJPAEntityManager;
import org.apache.openjpa.persistence.OpenJPAPersistence;
import org.apache.openjpa.persistence.jdbc.IsolationLevel;
import org.apache.openjpa.persistence.jdbc.JDBCFetchPlan;
import org.springframework.jdbc.datasource.ConnectionHandle;
import org.springframework.jdbc.datasource.ConnectionHolder;
import org.springframework.jdbc.support.JdbcUtils;
import org.springframework.orm.jpa.DefaultJpaDialect;
import org.springframework.transaction.SavepointManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionException;

/**
 * {@link org.springframework.orm.jpa.JpaDialect} implementation for Apache OpenJPA. Developed and tested against
 * OpenJPA 2.2.
 *
 * @author Costin Leau
 * @author Juergen Hoeller
 * @since 2.0
 */
public class OpenJpaDialect extends DefaultJpaDialect {

    @Override
    public Object beginTransaction(EntityManager entityManager, TransactionDefinition definition)
            throws PersistenceException, SQLException, TransactionException {

        OpenJPAEntityManager openJpaEntityManager = getOpenJPAEntityManager(entityManager);

        if (definition.getIsolationLevel() != TransactionDefinition.ISOLATION_DEFAULT) {
            // Pass custom isolation level on to OpenJPA's JDBCFetchPlan
            // configuration
            FetchPlan fetchPlan = openJpaEntityManager.getFetchPlan();
            if (fetchPlan instanceof JDBCFetchPlan) {
                IsolationLevel isolation = IsolationLevel.fromConnectionConstant(definition.getIsolationLevel());
                ((JDBCFetchPlan) fetchPlan).setIsolation(isolation);
            }
        }

        entityManager.getTransaction().begin();

        if (!definition.isReadOnly()) {
            // Like with EclipseLink, make sure to start the logic transaction
            // early so that other
            // participants using the connection (such as JdbcTemplate) run in a
            // transaction.
            openJpaEntityManager.beginStore();
        }

        // Custom implementation for OpenJPA savepoint handling
        return new OpenJpaTransactionData(openJpaEntityManager);
    }

    @Override
    public ConnectionHandle getJdbcConnection(EntityManager entityManager, boolean readOnly) throws PersistenceException, SQLException {

        return new OpenJpaConnectionHandle(getOpenJPAEntityManager(entityManager));
    }

    /**
     * Return the OpenJPA-specific variant of {@code EntityManager}.
     *
     * @param em
     *            the generic {@code EntityManager} instance
     * @return the OpenJPA-specific variant of {@code EntityManager}
     */
    protected OpenJPAEntityManager getOpenJPAEntityManager(EntityManager em) {
        return OpenJPAPersistence.cast(em);
    }

    /**
     * Transaction data Object exposed from {@code beginTransaction}, implementing the {@link SavepointManager}
     * interface.
     */
    private static class OpenJpaTransactionData implements SavepointManager {

        private final OpenJPAEntityManager entityManager;

        private int savepointCounter = 0;

        OpenJpaTransactionData(OpenJPAEntityManager entityManager) {
            this.entityManager = entityManager;
        }

        @Override
        public Object createSavepoint() throws TransactionException {
            this.savepointCounter++;
            String savepointName = ConnectionHolder.SAVEPOINT_NAME_PREFIX + this.savepointCounter;
            this.entityManager.setSavepoint(savepointName);
            return savepointName;
        }

        @Override
        public void rollbackToSavepoint(Object savepoint) throws TransactionException {
            this.entityManager.rollbackToSavepoint((String) savepoint);
        }

        @Override
        public void releaseSavepoint(Object savepoint) throws TransactionException {
            try {
                this.entityManager.releaseSavepoint((String) savepoint);
            } catch (Throwable ex) {
                LogFactory.getLog(OpenJpaTransactionData.class).debug("Could not explicitly release OpenJPA savepoint", ex);
            }
        }
    }

    /**
     * {@link ConnectionHandle} implementation that fetches a new OpenJPA-provided Connection for every
     * {@code getConnection} call and closes the Connection on {@code releaseConnection}. This is necessary because
     * OpenJPA requires the fetched Connection to be closed before continuing EntityManager work.
     *
     * @see org.apache.openjpa.persistence.OpenJPAEntityManager#getConnection()
     */
    private static class OpenJpaConnectionHandle implements ConnectionHandle {

        private final OpenJPAEntityManager entityManager;

        OpenJpaConnectionHandle(OpenJPAEntityManager entityManager) {
            this.entityManager = entityManager;
        }

        @Override
        public Connection getConnection() {
            return (Connection) this.entityManager.getConnection();
        }

        @Override
        public void releaseConnection(Connection con) {
            JdbcUtils.closeConnection(con);
        }
    }

}
